Skip to content

Add Notice JSON serialize/deserialize API with round-trip test#127

Open
vLuckyyy wants to merge 1 commit intomasterfrom
feature/notice-json-serialization-api
Open

Add Notice JSON serialize/deserialize API with round-trip test#127
vLuckyyy wants to merge 1 commit intomasterfrom
feature/notice-json-serialization-api

Conversation

@vLuckyyy
Copy link
Member

No description provided.

@vLuckyyy vLuckyyy requested a review from a team as a code owner February 26, 2026 17:34
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @vLuckyyy, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the Notice system by introducing robust JSON serialization and deserialization capabilities. This allows Notice objects to be easily converted into JSON strings for storage, transmission, or inter-process communication, and then accurately reconstructed from those strings. The addition of these features improves the flexibility and utility of the Notice class, making it more adaptable for various application needs.

Highlights

  • JSON Serialization/Deserialization: Introduced robust JSON serialization and deserialization capabilities for Notice objects, allowing them to be converted to and from JSON strings.
  • Gson Dependency: Added the Gson library as a new dependency to facilitate efficient JSON processing.
  • Dedicated Codec: Implemented a new NoticeJsonCodec class to encapsulate the logic for converting Notice objects to JSON and vice-versa.
  • Round-trip Test: Included a comprehensive test case to validate the correctness of the serialization and deserialization process by performing a round-trip conversion.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • buildSrc/src/main/kotlin/Versions.kt
    • Added GSON version constant.
  • multification-core/build.gradle.kts
    • Added com.google.code.gson:gson as an implementation dependency.
  • multification-core/src/com/eternalcode/multification/Multification.java
    • Imported Notice class.
    • Added serialize(Notice notice) and deserialize(String raw) methods to the Multification class, delegating to Notice's new methods.
  • multification-core/src/com/eternalcode/multification/notice/Notice.java
    • Imported NoticeResolverDefaults and NoticeResolverRegistry.
    • Added a static DEFAULT_NOTICE_REGISTRY field.
    • Implemented serialize() and serialize(NoticeResolverRegistry noticeRegistry) methods.
    • Implemented static deserialize(String raw) and deserialize(String raw, NoticeResolverRegistry noticeRegistry) methods.
  • multification-core/src/com/eternalcode/multification/notice/NoticeJsonCodec.java
    • New file created to encapsulate the logic for serializing and deserializing Notice objects to/from JSON using Gson.
    • Contains static methods serialize and deserialize that handle the conversion of NoticePart objects to JsonElement and vice-versa, managing different NoticeSerdesResult types (Single, Multiple, Section, Empty).
  • multification-core/test/com/eternalcode/multification/MultificationTest.java
    • Imported java.time.Duration and org.junit.jupiter.api.Assertions.assertEquals.
    • Added a new test method shouldSerializeAndDeserializeNoticeAsJson to verify the round-trip serialization and deserialization of a Notice object, asserting that the original and restored Notice objects have identical parts.
Activity
  • No specific activity (comments, reviews, progress updates) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 596f580fa0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for (Map.Entry<String, JsonElement> entry : root.entrySet()) {
String key = entry.getKey();
NoticeSerdesResult result = toSerdesResult(key, entry.getValue());
Optional<NoticeDeserializeResult<?>> deserialized = noticeRegistry.deserialize(key, result);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard unknown notice keys before delegating deserialization

When deserializing arbitrary JSON, this line calls noticeRegistry.deserialize(key, result) before validating that key is registered; for unknown keys, NoticeResolverRegistry.deserialize dereferences a null resolver set and throws a NullPointerException, so the intended IllegalArgumentException("Unsupported notice key") path is never reached. Any typo or extra field in input JSON will crash deserialization with an opaque NPE instead of a controlled validation error.

Useful? React with 👍 / 👎.

Comment on lines +81 to +83
if (result instanceof NoticeSerdesResult.Empty) {
return JsonNull.INSTANCE;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve empty chat entries in JSON round-trip

Serializing NoticeSerdesResult.Empty as JSON null causes round-trip failure for valid notices with empty chat content (e.g., Notice.builder().chat(List.of()).build()): deserialization maps null back to Empty, ChatResolver.deserialize returns Optional.empty(), and the codec then throws Unsupported notice key. This means the new serialize/deserialize API cannot reliably restore all notices it can serialize.

Useful? React with 👍 / 👎.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces JSON serialization and deserialization for Notice objects. While this is a great addition, a potential Denial of Service vulnerability was identified. The deserialization process is susceptible to a NullPointerException when encountering unknown keys in the input JSON, due to a bug in the underlying NoticeResolverRegistry. This could be exploited by an attacker providing malformed JSON to crash the application or the thread processing the notice. Additionally, there are suggestions in NoticeJsonCodec.java to leverage modern Java 17 features like switch expressions and streams for improved code conciseness and maintainability.

for (Map.Entry<String, JsonElement> entry : root.entrySet()) {
String key = entry.getKey();
NoticeSerdesResult result = toSerdesResult(key, entry.getValue());
Optional<NoticeDeserializeResult<?>> deserialized = noticeRegistry.deserialize(key, result);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The deserialize method calls noticeRegistry.deserialize(key, result), which can lead to a NullPointerException if the key provided in the JSON is not registered in the noticeRegistry. This is because NoticeResolverRegistry.deserialize (line 37 in the context file) does not check if the returned set of resolvers is null before iterating over it. An attacker can provide a malicious JSON string with an unknown key to cause the application to throw an unhandled exception, potentially leading to a Denial of Service (DoS) if the exception is not caught by the caller.

Comment on lines +60 to +86
private static JsonElement toJsonElement(NoticeSerdesResult result) {
if (result instanceof NoticeSerdesResult.Single single) {
return new JsonPrimitive(single.element());
}

if (result instanceof NoticeSerdesResult.Multiple multiple) {
JsonArray array = new JsonArray();
for (String element : multiple.elements()) {
array.add(element);
}
return array;
}

if (result instanceof NoticeSerdesResult.Section section) {
JsonObject object = new JsonObject();
for (Map.Entry<String, String> sectionEntry : section.elements().entrySet()) {
object.addProperty(sectionEntry.getKey(), sectionEntry.getValue());
}
return object;
}

if (result instanceof NoticeSerdesResult.Empty) {
return JsonNull.INSTANCE;
}

throw new IllegalArgumentException("Unsupported result type: " + result.getClass().getName());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The if-instanceof chain can be replaced with a switch expression. Since NoticeSerdesResult is a sealed interface and the project uses Java 17, a switch expression is more concise and provides compile-time safety for exhaustiveness.

    private static JsonElement toJsonElement(NoticeSerdesResult result) {
        return switch (result) {
            case NoticeSerdesResult.Single single -> new JsonPrimitive(single.element());
            case NoticeSerdesResult.Multiple multiple -> {
                JsonArray array = new JsonArray();
                multiple.elements().forEach(array::add);
                yield array;
            }
            case NoticeSerdesResult.Section section -> {
                JsonObject object = new JsonObject();
                section.elements().forEach(object::addProperty);
                yield object;
            }
            case NoticeSerdesResult.Empty empty -> JsonNull.INSTANCE;
        };
    }

Comment on lines +108 to +117
private static List<String> toStringList(String key, JsonArray jsonArray) {
List<String> values = new ArrayList<>();
for (JsonElement jsonElement : jsonArray) {
if (!jsonElement.isJsonPrimitive() || !jsonElement.getAsJsonPrimitive().isString()) {
throw new JsonParseException("All array elements for key '" + key + "' must be strings");
}
values.add(jsonElement.getAsString());
}
return values;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method can be simplified by using Java Streams. This would make the code more declarative and potentially more readable.

    private static List<String> toStringList(String key, JsonArray jsonArray) {
        return jsonArray.asList().stream()
            .map(jsonElement -> {
                if (!jsonElement.isJsonPrimitive() || !jsonElement.getAsJsonPrimitive().isString()) {
                    throw new JsonParseException("All array elements for key '" + key + "' must be strings");
                }
                return jsonElement.getAsString();
            })
            .toList();
    }

Comment on lines +119 to +129
private static Map<String, String> toStringMap(String key, JsonObject jsonObject) {
Map<String, String> values = new LinkedHashMap<>();
for (Map.Entry<String, JsonElement> jsonEntry : jsonObject.entrySet()) {
JsonElement jsonElement = jsonEntry.getValue();
if (!jsonElement.isJsonPrimitive() || !jsonElement.getAsJsonPrimitive().isString()) {
throw new JsonParseException("All object values for key '" + key + "' must be strings");
}
values.put(jsonEntry.getKey(), jsonElement.getAsString());
}
return values;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method can be refactored to use Java Streams and Collectors.toMap for a more functional and concise implementation. This maintains the use of LinkedHashMap to preserve insertion order.

    private static Map<String, String> toStringMap(String key, JsonObject jsonObject) {
        return jsonObject.entrySet().stream()
            .collect(java.util.stream.Collectors.toMap(
                Map.Entry::getKey,
                entry -> {
                    JsonElement jsonElement = entry.getValue();
                    if (!jsonElement.isJsonPrimitive() || !jsonElement.getAsJsonPrimitive().isString()) {
                        throw new JsonParseException("All object values for key '" + key + "' must be strings");
                    }
                    return jsonElement.getAsString();
                },
                (v1, v2) -> v2,
                LinkedHashMap::new
            ));
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant